浅谈Linux IO复用

1. Linux IO简介

1.1 IO概述

IO是Input、Output的简称,也就是输入和输出。在计算机世界中,除了计算和存储,剩下的几乎都是IO,它包含硬件设备层面的数据交换,也包含软件层面的数据传输。

1.2 Linux IO方式

Linux IO方式表示Linux系统处理IO事件的方式,IO事件包括可读事件、可写事件、错误事件等。Linux IO方式主要包括:

  • 阻塞IO:在IO事件发生之前,会一直阻塞。
  • 非阻塞IO:如果没有IO事件,直接返回错误。
  • IO多路复用:同时监听多个描述符的IO事件,取出状态ready的描述符列表。
  • 异步IO:为IO事件绑定处理程序,当事件发生时触发处理程序。
  • 信号驱动IO:利用Linux的信号机制,当IO事件发生时,触发信号处理程序。
  • 等。

2. IO复用

2.1 概念

IO复用(I/O Multiplexing)通俗的来说:是同时处理多个描述符IO事件的一种技术手段。这些文件描述符包括:socket套接字、普通文件、设备文件等。

举个简单例子:tcp server同时处理两个文件描述符,一个是标准输入,一个是tcp连接。当server接收标准输入时,可能会因调用fgets()而阻塞,从而无法及时处理另一个tcp连接的可读事件,比如有tcp client发送了数据。

如果tcp server想要同时处理多个描述符的事件,可能的做法是开启多个线程或进程,各自等待描述符的可读可写事件,但这样一来,就需要引入线程间同步和通信问题,大大增加编程的复杂性。

但IO复用技术的出现,可以很好的解决上述问题,它直接管理多个描述符,选出IO事件ready的描述符列表。这种操作方式允许应用程序以较低的成本、较高的效率,同时管理多个描述符的IO事件。

在Linux中,以下接口可以实现IO多路复用:

  • select
  • poll
  • epoll

2.2 select

select函数的函数签名如下:

1
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

它监听一组描述符,并在指定时间内选出可读、可写或者异常的描述符列表。参数介绍如下;

  • timeout:select函数执行的时间,在这个时间范围内选出ready的描述符列表。
  • readfds、writefds、exceptfds:关心的描述符集合的指针,包括可读、可写、异常的描述符集合。可以通过FD_ISSET宏来判断描述符是否ready。
  • nfds:监听的描述符集合的最大值+1,类似于描述符范围的上限。

大致处理过程:

  • 将用户空间的fdsets拷贝到内核空间,并分配结果buffer。
  • 遍历fdsets所有fd,找到对应的文件对象file指针,通过poll()检查描述符的事件是否满足条件。
  • 检查超时、遍历完后,将满足条件的fd放入结果buffer中。
  • 拷贝结果到用户空间,并释放之前申请的内存。

2.3 poll

poll和select的原理没啥本质不同,只是实现的方式更加优雅,封装了很多细节。其函数签名如下:

1
2
3
4
5
6
7
8
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

struct pollfd {
int fd;
short events;
short revents;
};

将监听的描述符以数组的方式传入,并为每个描述符绑定了监听事件。

2.4 epoll

select和poll在描述符集合量级较小的情况下,性能表现还可以,但如果监听的描述符集合太大,就需要考虑epoll了。epoll的用法如下:

  1. 创建epoll的句柄,size是监听描述符的个数。
1
int epoll_create(int size);
  1. 对监听的描述符操作
1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数解释如下:

  • epfd: epoll_create返回的句柄
  • op: 标识操作类型
    • EPOLL_CTL_ADD: 添加
    • EPOLL_CTL_DEL: 删除
    • EPOLL_CTL_MD: 修改
  • fd: 文件描述符
  • event: 需要监听的事件。
  1. 监听事件的发生
1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • epfd:等待epfd上的io事件,最多返回maxevents个事件
  • events:用来从内核得到事件的集合
  • maxevents:events数量,该maxevents值不能大于创建epoll_create()时的size
  • timeout:超时时间(毫秒,0会立即返回)

epoll有两种工作模式

  • LT(水平触发,默认):当时间发生时,如果用户不处理的话,内核会一直通知此事件。
  • ET(垂直触发):只有当事件状态发生改变时,内核才会通知。(如果描述符缓冲区还有未处理的数据,内核下次不会再通知了,因为状态没有发生改变)

epoll的工作处理流程,大致如下:

  • 当新增描述符监听时,也就是调用EPOLL_CTL_ADD,内核会将初始化一个epitem(保存fd、监听事件等等),插入到红黑树中,并将回调函数ep_poll_callback通过vfs_poll注册到描述符的等待队列中。
  • 当描述符ready后,会调用回调函数ep_poll_callback,它会将描述符对应的epitem拷贝到rdllist中。
  • 调用epoll_wait其实是检查rdllist的过程。

2.5 select vs epoll

从工作原理中,也可以看出select与epoll的诸多不同,优缺点对比如下:

  • 支持监听的描述符数量
    • select支持的描述符数量受限:因为需要遍历描述符列表、多次用户空间到内核空间的内存拷贝。
    • epoll可以支撑监听海量的描述符。
  • 性能
    • select随着描述符数量的增多,性能下降严重,时间复杂度 > O(n)。
    • epoll通过回调函数的方式,在描述符增长的情况下,依旧有出色的性能。
  • 使用场景
    • select使用方式较为简单,适用于描述符量级小的情况。
    • epoll适用于同时监听大量的描述符。

3. 总结

IO模式多种多样,在软件开发过程中,不能唯一而论,要根据实际的应用场景,选择合适的IO技术,才能取得事半功倍的效果。